RPi Hide and Seek

Alan Hsiao (ah668) and Grace Tan (gnt4)
ECE5725 Final Project
Fall 2020


Demonstration Video


Project Overview

This project takes the traditional game of hide-and-seek at home and merges it with technology and the outdoors.
Players need a phone with a cellular hotspot to play. Each player (a hider and a seeker) will use a Raspberry Pi system and a GPS module.
The hider will have 60 seconds to hide before each player will have the ability to see how far away the other player is.
Each player will randomly receive power ups during the game that will "zap" and temporarily disable the other player's screen.
Both the hider and seeker can move during the "seeking" phase of the game, but the hider must walk.

Important Objectives


Design & Testing

Development Timeline

Since there are many components to this project, smaller python modules were made and tested independently.
Then, individual python modules were integrated together.

Below is a timeline of the progress of the project.

Hardware

Item Part Number Quantity Cost/Unit Total Cost
GPS Module Receiver NEO-6M NEO-6M 2 $10.99 $21.98
2.8" TFT (320x240) with Resistive Touchscreen 2298 2 Free ($34.95) Free ($69.90)
Raspberry Pi 4 Model B - 1 GB RAM 4295 2 Free ($30.00) Free ($60.00)
Portable Charger Battery (2-Pack) M850 1 Free ($14.99) Free ($24.99)
USB-C Cable (2-Pack) USB-C-G1 1 Free ($5.99) Free ($5.99)

Hardware in the system is composed of two subsystems, which each have a GPS module, Raspberry Pi 4, Pi TFT, battery pack, and USB-C Cable.
Since the only piece of hardware that needed to be purchased were the GPS modules, the total for this project was $21.98.

Software

Software in the system includes python modules for the hider and seeker. For various functionality, the following python packages were used:

RPi Communication

When we first came up with the idea for this project, we envisioned our system using TCP or UDP to communicate between the two Raspberry Pis through the use of mobile hotspot. However, we knew that we would have to break the problem down into smaller problems by adopting an incremental development design methodology. The first iteration of our testing, we developed scripts that could communicate between two computers on the same WiFi network which allowed us to develop and test code faster. This was done easily with the Python socket library to establish a TCP connection or send messages using UDP. This code is available in the code appendix as tcp_ip_server.py, tcp_ip_client.py, udp_server.py, and udp_client.py.

Both UDP and TCP required the server and the client to connect to a specific port on a specific IP address. Due to the design of the protocols, TCP is more reliable since it can verify whether or not the host receives a message whereas for UDP, the client never knows whether the message is received. Since TCP is a stream-oriented connection, the client and host must establish a connection to send a stream of bytes, it has a larger overhead compared to UDP (which solely uses datagrams). This led us to use UDP for our programs because of strict real-time requirements and relatively flexible packet drop constraints.

The second obstacle that we encountered was trying to connect two laptops on two different WiFi networks. Since the laptops are now on separate networks, we are not able to use their private IP addresses as the IP address in the code. Instead, we must utilize the router’s public IP address and change the settings to allow for port forwarding. This allows a computer to send data to a specific port (in this case, port 20-21) on the other network’s router which will then forward the data to the other player’s computer. We were able to successfully send messages to each other’s devices while on two different networks after changing the router's setting as shown below.

port forwarding

The next step was to find a way to change port forwarding settings on our mobile hotspots. Since we were originally using our home networks, it was easy to change port forwarding settings. However, as we did more research, we realized that mobile hotspots are protected behind an Internet Service Provider (ISP) firewall which does not allow for port forwarding (unless you pay a significant amount). We had to quickly change gears and try a different way to communicate between the two Raspberry Pis.

After looking at many different Python libraries, we settled on using the class server as a meeting point for the two Raspberry Pis. We created a Secure Shell (SSH) client on both Raspberry Pis to write and read files on the server as a way to communicate. We leveraged a library called paramiko which requires the IP address, username, and password to connect to a server. A bug that we ran into is that if a file is read while it's being written to, the readline will return as an empty string. We were able to quickly patch our program by only taking in non-empty strings as parsed data. In order to simplify the synchornization, we decided to design our communication protocol similar to UART where one file is used for sending data and another file is used for receiving data. Additionally, we were able to easily send different types of data by using comma-delimited formatting. Our initial tests were a success and can be viewed or executed with the serverRW.py file. This establishes the communication link between the two RPi systems as shown below.

system architecture

We first tested RPi communication indoors and then moved to using hotspot and doing so outdoors. While testing the fully integrated system with the GPS modules and pygame logic, we ran into issues of the screen freezing halfway through playing the game outside. At first, we thought it was a temperature problem with the RPi or GPS module. However, after looking at the data sheet, we saw that the cold temperature was not the issue. Finally, we figured out that the RPi froze in displaying the pygame screens because it was switching from our hotspot to wifi connection even when we were outside. Thus, we made sure to play this game away from any wifi connections the RPi may have when doing final testing.

GPS Data

The first GPS goal was to communicate with the GPS module and parse the data. We needed to get both working individually on each Raspberry Pi. A challenge that we faced was cold-starting the GPS module as these GPS modules were brand new and made in China and acquiring signal took more than 20 minutes of waiting outside. However, once the GPS modules acquired signal, they store the general location in memory such that if they are restarted, they will not need to cold-start again. The next time we tried starting the GPS modules, it only took 1 minute to acquire a GPS lock. By using a micro-USB cable, we were able to simplify the hardware and avoid circuit connectivity issues with headers.

To find which port to read from, we first looked at the devices and on the bus by typing in the command “lsusb” and saw that the GPS module corresponds to U-Blox AG, device 4 as shown below.

lsusb

Next, to find the associated driver, we used the “usb-devices” command, which displays detailed information on the devices connected to the USB. Below is part of the output that corresponds to the GPS module. We saw that the driver is cdc_acm so when looking at the various drivers under the dev directory, we know that the ttyACM0 is the corresponding driver.

usb-devices

We used the serial library to read the data from the GPS module as well as the pynmea2 library to parse the GPS data. We parse each line and read the message if the sentence contains ‘GGA’, which means the sentence indicates the fixed data in GGA NMEA format for the GPS module, allowing us to obtain GPS coordinates.

Now that the GPS coordinates can be obtained, we determined the distance using the Haversine formula, as shown below. We saw that at best, when the GPS modules were directly next to each other, we were able to obtain a measurement of about 3-9 meters. In order to try to obtain more accurate data, we looked at the average altitude in collegetown to replace the averaged Earth’s radius, but found that the mean radius actually produced better results than the collegetown radius. If we had additional money to spend on this project, we would purchase a differential or carrier-phase tracking GPS module to get centimeter accuracy. We believe that with additional tweaking, we could possibly get a more accurate reading, but for $10 GPS modules from Amazon operating in the Ithaca weather, we believe this is a reasonable accuracy.

One issue that we ran into involved reading the GGA sentences from the GPS module. While creating the initial test script, we used an if statement to check if the sentence was in GGA format. However, as we designed the game, we saw that our GPS location was updating much more slowly than usual. This was because our module would simply check the sentence for GGA format, and if it wasn't, it would continue on to the rest of the game logic. Since GGA sentences are only sent so often, we would be missing a lot of the GPS location updates as we computed the game logic. While debugging the game, we changed the if statement to a while loop in order for the game to receive as many GGA sentences as possible.

Haversine Formula

Game Finite State Machine

Our next goal was to develop an FSM description of our game such that we could simplify the code implementation. This was also helpful in creating screens for each different state. Each player has a control variable, which take the value from -1 to 7 inclusive. Thus, each state is based on the combination of each player's control variable. This control variable is communicated on the server so that each player can advance to the correct state in real time. Regardless of the control variable, each player will read and write its current state to the sever each time the screen updates. This ensures that the server as well as the Raspberry Pi both have the most up to date copy of the game variables. Below is the diagram of the finite state machine (FSM), which can be further broken down into the pre-game portion (top of the FSM), the game portion (bottom right of FSM), and the post-game portion (bottom left of FSM).

FSM

Pre-Game

The pre-game section starts when one of the players join up until the countdown when the game starts. When the player is not in the game, his or her control variable is -1. Once the player enters the game, their control variable updates to 0. If the other player has not entered the game, there is a note in small red text to inform the player that the other player has not joined. Having this note enhances the player experience so they are aware of the other player's state. The color red was chosen to catch the player's attention but the text was smaller so it would not distract from the other parts of the screen, which are more important. The more important sections are displayed in white text on a black background for best contrast. The center is where part of the screen is where the distance is displayed. During the pre-game portion, there is no distance since the game has not started and so the display shows "-- m". The right side shows the menu options, which corresponding to the 4 GPIO pins on the PiTFT display. During any portion of the game, the player can quit, and so there is a "QUIT" option even if there is only one player in the game. This "QUIT" button is the bottom most as the exit option is usually placed at the bottom-right-most portion of the screen.

Once both players enter into the game, they start in the "Initialization" state. The note is updated to reflect which player is either hider or seeker and the menu has "READY" option at the top. When a player presses the "READY" option, his or her control variable is updated to 1 and written to the server. Additionally, the local time for when the button is pressed recorded and written to the server. This way, when a second player presses the ready button, they can make a comparison between their recorded time and the other player’s recorded time. We programmed the RPi to always reference the newer time (when the second player presses the ready button) as the “start” of the game which solves synchronization and locking issues. This corresponds to the "Communicate Countdown Time" state. There is also a note displayed that notifies the current player that the other player is waiting for them to press "READY".

From there, both players transition to the "Hiding Countdown" state where the hiding countdown starts for 60 seconds. Since we have synchronized the game “start time” between the two RPis, we do not have to worry about latency or communication issues since the time is both locally kept as well as synchronized over the internet network. During the countdown, the seeker will stay in place for a minute while waiting for the hider to hide.

At any point in this countdown, if the hider feels that they are ready for the seeker to start, they can press the start button to initialize a game start countdown. This is the "Hider Finish Early with Game Starting Countdown" state and the hider’s control variable is updated to 3, which initializes a 5 second countdown timer with a note to inform the seeker that the hider is done hiding. Otherwise, the game will continue to countdown until 60 seconds have passed. Below are the screens for the pre-game portion.

Init Screens

Game

The game section starts when the countdown ends and the seeker starts to look for the hider. At this point, both the hider and the seeker can move, but the hider can only walk. The seeker will be able to use the relative distance to get an idea of the general location of the hider while the hider will know if the seeker is getting closer or further away. Both players' control variables are set to 4 during the play game phase. During this state, each player's RPi will read the serial port to obtain the GPS data. It will wait until the GPS data sent is a NMEA GGA sentence (which means that GPS lock has been acquired) before parsing it. We use the pynmea2 library for parsing the data into latitude and longitude. The latitude and longitude are then passed into the Haversine formula (along with the other Pi's location which was read from the server). If there is no GPS signal, then the calculated distance will be absurdly large and the number will be replaced by "## m" on the screen.

A critical feature to have in any type of game is the ability to pause. We implemented our pause functionality by changing to another state. When one player presses the pause button during the game, their control variable will change to 5. This causes both their screen as well as the other player's screen to show the "PAUSED" text. While the game is paused, the other player also has the option to press pause if they must attend to anything, but both players must be unpaused (hit the resume button) in order for the game to start again. Additionally, there will be a note at the bottom of the screen indicating which players have hit pause. Similar to the pre-game state, the ability to quit at any time is still in place.

Another feature that we implemented in the play game state is the ability to gain power ups. Each time the screen updates, the code will roll a random number between 0 and 100. If the number is 0, the player will receive a power up called "ZAP". This power up can be used by pressing the button and will temporarily disable the other player's ability to pause and see the relative distance. The zap feature was implemented by briefly changing the player's control variable to 7 for 10 seconds. While the other player's control variable is 7, the current player's screen will show "?? m" instead of the actual distance. We noticed that this gives at least one player a zap about once every minute and is a good pace for fast-paced games. One thing to note that is if a player is zapped while having a power up, they can still zap the other player while they are zapped.

Since it can still sometimes be difficult to find the hider if they are good at hiding, we wanted to implement a confirmation from the seeker that they have indeed found the hider. Once the seeker is less than 10 meters away from the hider, they will see a button on the right side of the screen to "FINISH" the game. Pressing the "FINISH" button will move the game into "post-Game" section by setting the seeker's control variable to 6. The hider's Raspberry Pi will then see that the seeker has found the hider and also set its control variable to 6.

Game Screens

Post-Game

For the post-game section, players will see that the game is over because the hider has been found. There are several options that players can choose from at this point. They can either play again, quit the game, or wait to see what the other player wants to do. If a player presses restart, they will be taken back to the initialization screen and their control variable will be set to 0. Additionally, the other player that has yet to hit the restart button will see a message that the other player wants to play again.

If at any point, for any reason, one of the players does not want to continue to play the game, they can press the quit button twice to leave the game. If they only press quit once, a quit confirm message will remain on the screen for 5 seconds and then revert back to requiring two quit button presses to leave the game. When one player has quit the game, the other player will also see a note saying that the other player has left the game to ensure that neither player is waiting too long to determine the other player's intentions. Similar to what was mentioned in the pre-game section, players that choose to quit the game will set their control variable to -1 before exiting to ensure that the other player knows.

Finish Screens

Results & Conclusion

After many hours of design, testing, and research, the Raspberry Pi Hide and Seek game is fully working and a great way to spend time outdoors with family and friends during quarantine. The system works as intended and gives players a responsive and easy to use graphical user interface (GUI). Additionally, the GPS modules are extremely responsive to movement and it was easy to tell when the seeker was starting to get closer and closer even while hiding behind objects. There are several key factors that make the game an enjoyable experience. First, as stated in the GPS Data section, we were able to bypass all jumpers and breadboards by purchasing a GPS module that had a micro-USB port. This allowed us to simply connect the micro-USB port on the GPS to a USB port on the Raspberry Pi and makes it much easier to run around without electrical failures. Second, the responsive GUI allows players to know the other player's intention without being next to them. The notes on the bottom of the screen allow you to quickly know what the other player wants to do. For example, if the other player wants to start the game or quit the game, it will show that on the screen. Lastly, our game is able to reduce unnecessary writes to the server during game play by only reading and writing every time the GPS module acquires a new lock or location. This extra precaution results in less server latency and faster distance calculation times.

Though we ran into several issues as we created this game, the end result works perfectly and meets the goals outlined in the description. To reiterate, several of the issues that we ran into include (but are not limited to): cold-starting the GPS module and acquiring a lock, finding the serial port of the GPS module, communicating between two computers on the same network, communicating between two computers on different networks, communicating between two computers on mobile hotspots, using the server as a communication point, writing and reading files at the same time, switching networks between WiFi and mobile hotspot, server latency due to excessive writing, GUI layout, parsing server and GPS data, communicating global time, and working out the power up logic. As explained in the RPi communication section, we realized somewhat late that we would not be able to use TCP/UDP for communicating between the two Raspberry Pis due to mobile hotspot, but we were able to successfully innovate around it and get a working game.

Future Work

If we had more time to explore additional implementations, we would add an intertial measurement unit (IMU) such that either the hider or seeker would only know the direction that the other player is coming from instead of the distance. Also, it would make the GUI easier to use if we integrated the code together and gave users the option to select if they want to be the hider or the seeker at the beginning of the game. Additionally, we thought about a harder version of the game where there are no distances displayed but there are colors where red indicates that the seeker is getting closer to the hider, blue for farther away, and green for within a 10 meter radius. Another thing that we could implement is experimenting with penalizing the hider if they are moving too fast. This could either be in the form of temporarily disabling their screen or giving the seeker more zaps than hider. Lastly, one additional feature that we hope to implement is the ability to have multiple hiders and as they are found, change their roles to seekers.


References

Item Description
GPS Module Purchasing GPS Module: NEO-6M
GPS Module Manual GPS Module: NEO-6M
PYNMEA2 Library GPS Sentence Parsing
Paramiko Library Server SSH and Reading/Writing
PyGame Library Graphical User Interface
Random Library Obtaining Power Ups
Haversine Formula Haversine Formula
Math Library Haversine Formula
Decimal Library Graphical User Interface
Linux USB USB Devices

Team

Generic placeholder image

Alan Hsiao

ah668@cornell.edu

BS in Electrical & Computer Engineer '21
MEng in Electrical & Computer Engineer '21

Work Distribution:

  • Hider Code
  • TCP/UDP Communication
  • Graphical User Interface
  • Server SSH
  • GPS Serial
  • Power Ups
  • START Button
  • PYNMEA2
  • Paramiko
  • Port Forwarding
  • System Architecture Diagram
Generic placeholder image

Grace Tan

gnt4@cornell.edu

BS in Electrical & Computer Engineer '20
MEng in Electrical & Computer Engineer '21

Work Distribution:

  • Seeker Code
  • TCP/UDP Communication
  • Graphical User Interface
  • Server SSH
  • GPS Serial
  • Power Ups
  • FINISH Button
  • PYNMEA2
  • Paramiko
  • Screenshots
  • FSM Diagram

Code Appendix

Below, we have provided our code for the hider and seeker. Our full repository with other code we used to help with incremental testing can be found here.

hiderFinal.py


    # Grace Tan (gnt4) and Alan Hsiao (ah668)
    # ECE 5725 - Embedded OS - Final Project
    # Wednesday Night Lab
    # Displaying screen for hider

    import os, sys, serial, time, math, random
    import RPi.GPIO as GPIO
    import pygame
    from pygame.locals import * # event MOUSE variables
    import pynmea2
    import paramiko
    import warnings
    from decimal import Decimal
    from cryptography.utils import DeprecatedIn25

    ############################################################################
    # RPI Communication 
    ############################################################################
    warnings.simplefilter('ignore', DeprecatedIn25)
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    # Changed to generic information for security purposes
    ssh.connect('class_sever_ip', username='my_netid', password='my_password')
    ftp = ssh.open_sftp()

    # GPS Module
    ser = serial.Serial('/dev/ttyACM0', 9600, timeout = 5)

    ############################################################################
    # Pygame 
    ############################################################################
    # Setup environment variables to display piTFT
    os.putenv('SDL_VIDEODRIVER', 'fbcon') # Display on piTFT
    os.putenv('SDL_FBDEV', '/dev/fb0') #
    os.putenv('SDL_MOUSEDRV', 'TSLIB') # Track mouse clicks on piTFT
    os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')

    # Start pygame
    pygame.init()
    pygame.mouse.set_visible(False)

    # Variables for colors
    WHITE = 255, 255, 255
    BLACK = 0, 0, 0
    RED = 255, 0, 0
    GREEN = 0, 255, 0
    BLUE = 0, 0, 255

    # Set screen variables
    size = width, height = 320,240
    screen = pygame.display.set_mode(size)
    center = 160,120

    # Fonts 
    my_font_distance = pygame.font.Font(None, 70)
    my_font_note = pygame.font.Font(None, 15)
    my_font_button = pygame.font.Font(None, 20)

    # Button 
    button1_text = 'READY'
    button2_text = 'QUIT'
    button4_text = 'ZAP'
    button1_pos = 290, 40
    button2_pos = 290, 225
    button4_pos = 290, 102
    my_buttons = {button1_text:button1_pos, button2_text:button2_pos}

    # Note
    note_text = "YOU ARE THE HIDER"
    note_pos = 160,180
    my_note = {note_text:note_pos}

    # Distance 
    distance_text = "-- m"
    distance_pos = center
    my_distance = {distance_text:distance_pos}

    # Function to display text 
    def update_text(font,text, pos, color):
        text_surface = font.render(text, True, color)
        rect = text_surface.get_rect(center = pos)
        screen.blit(text_surface,rect) 

    ############################################################################
    # GPIO
    ############################################################################
    # Set for broadcom numbering
    GPIO.setmode(GPIO.BCM)

    # Setup piTFT buttons from top to bottom
    GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_UP)

    # Define events
    def GPIO_17_callback(channel):
        global my_control, other_control, my_control_time
        # If you press READY
        if my_control == 0:
            my_control = 1
            my_control_time = time.time() + 64 #60 sec for actual game
        # If hider is ready first
        elif my_control == 2 and other_control == 2:
            my_control = 3
            my_control_time = time.time() + 5
        # If you press RESTART
        elif my_control == 6:
            my_control = 0
        # If you press PAUSE
        elif my_control == 4 and (other_control == 4 or other_control == 5):
            my_control = 5
        # If you press RESUME
        elif my_control == 5 and other_control >= 4:
            my_control = 4

    def GPIO_22_callback(channel):
        global my_control, has_powerup, powerup_time
        # If you press ZAP
        if has_powerup == 1:
            powerup_time = time.time() + 10
            my_control = 7
            has_powerup = 0

    def GPIO_27_callback(channel):
        global quit_confirmed, quit_pressed_time
        # If you press QUIT
        quit_confirmed += 1
        if quit_confirmed == 1:
            quit_pressed_time = time.time()    

    # Add events for GPIO
    GPIO.add_event_detect(17, GPIO.FALLING, callback=GPIO_17_callback, bouncetime=300)
    GPIO.add_event_detect(22, GPIO.FALLING, callback=GPIO_22_callback, bouncetime=300)
    GPIO.add_event_detect(27, GPIO.FALLING, callback=GPIO_27_callback, bouncetime=300)

    ############################################################################
    # Game
    ############################################################################
    # Game Control Variables
    # -1 -> not in game
    # 0  -> initialization
    # 1  -> pressed READY
    # 2  -> hider countdown
    # 3  -> hider finishes earlier - game starting countdown
    # 4  -> play game
    # 5  -> pressed PAUSE
    # 6  -> game over
    # 7  -> pressed ZAP
    my_control = 0
    other_control = 0

    # Other Game Variables
    my_control_time = 0     # Time to stop for countdowns
    other_control_time = 0  # Time to stop for countdowns
    powerup_time = 0        # Time when powerup finishes
    has_powerup = 0         # 1 when player has powerup and hasn't used it
    random_num = 1          # Random number rolled
    quit_confirmed = 0      # 0 = not pressed, 1 = pressed QUIT, 2 = quit confirm
    quit_pressed_time = time.time()
    init_time = time.time()

    # GPS Variables
    my_lat = 0
    my_lon = 0
    other_lat = 0
    other_lon = 0
    pi_distance = 0

    while quit_confirmed < 2:       

        # Write and read controls information to communicate to other RPi
        file_write=ftp.file('/home/gnt4/FinalProject/h2s.txt', "w", -1)
        file_write.write(str(my_control)+','+str(my_control_time)+','+str(my_lat)+','+str(my_lon))
        file_write.flush()
        file_read=ftp.file('/home/gnt4/FinalProject/s2h.txt',"r", -1)
        receive_controls = file_read.read()
        if (len(receive_controls) > 0):
            parsed_receive_controls = receive_controls.split(',')
            other_control = int(parsed_receive_controls[0])
            other_control_time = float(parsed_receive_controls[1])
            other_lat = float(parsed_receive_controls[2])
            other_lon = float(parsed_receive_controls[3])
        file_read.flush()

        # When QUIT is pressed, confirm quitting game with 5 second timeout
        if quit_confirmed == 1:
            button2_text = 'CONFIRM QUIT?'
            button2_pos = 255,225
            if (quit_pressed_time + 5 < time.time()):
                quit_confirmed = 0
                button2_text = 'QUIT'
                button2_pos = 290, 225
            my_buttons = {button1_text:button1_pos, button2_text:button2_pos}

        # When game is being played and unpaused
        if (my_control == 4 or my_control == 7) and (other_control == 4 or other_control == 7):
            if time.time() > powerup_time:
                my_control = 4

            # Acquire GPS Data
            gps_line = ser.readline()
            if len(gps_line) == 0:
                print("Time out! exit.\n")
                sys.exit()
            while gps_line.find('GGA') < 0:
                gps_line = ser.readline()

            gps_data = pynmea2.parse(gps_line)             
            my_lat = float(gps_data.latitude)
            my_lon = float(gps_data.longitude)

            # Calculate GPS Distance with Haversine Formula
            lat1 = math.radians(my_lat)
            lat2 = math.radians(other_lat)
            lon1 = math.radians(my_lon)
            lon2 = math.radians(other_lon)
            dlon = lon2 - lon1
            dlat = lat2 - lat1
            earth_radius = 6373 # Average earth radius in km
            h1 = pow(math.sin(dlat/2),2) + math.cos(lat1) * math.cos(lat2) * pow(math.sin(dlon/2),2)
            h2 = 2 * math.atan2( math.sqrt(h1), math.sqrt(1-h1) )
            pi_distance = round((earth_radius * h2 * 1000),2)
        
            # When a random number is generated since there is currently no powerup
            if has_powerup == 0:
                random_num = random.randint(0,100)
            # if you get powerup
            if random_num == 0:
                has_powerup = 1
                button4_text = 'ZAP'
            else:
                button4_text = ''

            # Print distance unless if there is no signal]
            if pi_distance > 10000:
                my_distance = {'## m':distance_pos}
            elif other_control == 7:
                my_distance = {'?? m':distance_pos}
            else:                
                my_distance = {str(pi_distance) + ' m':distance_pos}

            # Clear pause note
            my_note = {note_text:note_pos}
            if (other_control == 4 and my_control != 7):
                my_buttons = {'PAUSE':button1_pos, button2_text:button2_pos, button4_text:button4_pos}
            else:
                my_buttons = {'':button1_pos, button2_text:button2_pos, button4_text:button4_pos}

        # If other player finds you
        elif other_control == 6 and my_control == 4:
            my_control = 6

        # If you and the other player have not restarted
        elif my_control == 6 and other_control == 6:
            my_distance = {'GAME OVER':distance_pos}
            my_note = {'THANKS FOR PLAYING!':note_pos}
            my_buttons = {'RESTART':button1_pos, button2_text:button2_pos}
        
        # If you have not restarted and the other player has
        elif my_control == 6 and (other_control == 0 or other_control == 1):
            my_distance = {'GAME OVER':distance_pos}
            my_note = {'SEEKER WANTS TO PLAY AGAIN!':note_pos}
            my_buttons = {'RESTART':button1_pos, button2_text:button2_pos}
        
            # If you have not restarted and the other player has
        elif my_control == 6 and (other_control == -1):
            my_distance = {'GAME OVER':distance_pos}
            my_note = {'SEEKER HAS LEFT THE GAME':note_pos}
            my_buttons = {'RESTART':button1_pos, button2_text:button2_pos}

        # If game paused by both players
        elif my_control == 5 and other_control == 5:
            my_distance = {'PAUSED':distance_pos}
            my_note = {'SEEKER ALSO PAUSED':note_pos}
            my_buttons = {'RESUME':button1_pos, button2_text:button2_pos}
            
        # If game paused only by you
        elif my_control == 5 and other_control == 4:
            my_distance = {'PAUSED':distance_pos}
            my_note = {'':note_pos}
            my_buttons = {'RESUME':button1_pos, button2_text:button2_pos}

        # If game paused only by other player
        elif my_control == 4 and other_control == 5:
            my_distance = {'PAUSED':distance_pos}
            my_note = {'WAITING FOR SEEKER TO UNPAUSE...':note_pos}
            my_buttons = {'PAUSE':button1_pos, button2_text:button2_pos}
        
        # Game Countdown
        elif my_control == 3:
            if my_control_time > time.time():
                my_distance = {str(int(my_control_time - time.time())) + " s":distance_pos}
                my_note = {'GAME IS STARTING':note_pos}
                my_buttons = {'':button1_pos, button2_text:button2_pos}
            else: 
                my_control = 4
        
        # Hider Hiding
        elif my_control == 2 and other_control >= 2:
            if my_control_time > time.time():
                my_distance = {str(int(my_control_time - time.time())) + " s":distance_pos}
                my_note = {'HIDE BEFORE THE COUNTDOWN ENDS':note_pos}
                my_buttons = {'START':button1_pos, button2_text:button2_pos}
            else: 
                my_control = 4

        # If both players ready, start countdown
        elif my_control == 1 and other_control >= 1:
            if(my_control_time < other_control_time):
                my_control_time = other_control_time
            my_control = 2

        # You are not ready but other player is
        elif my_control == 0 and other_control == 1:
            my_note = {'SEEKER IS READY':note_pos}

        # You are ready but other player is not
        elif my_control == 1 and other_control == 0:
            my_distance = {'-- m':distance_pos}
            my_note = {'SEEKER IS NOT READY':note_pos}
            my_buttons = {'':button1_pos, button2_text:button2_pos}

        # When other player leaves game
        elif other_control == -1:
            my_control = 0
            my_distance = {'-- m':distance_pos}
            my_note = {'SEEKER IS NOT IN GAME':note_pos}
            my_buttons = {'':button1_pos, button2_text:button2_pos}

        else:
            my_distance = {'-- m':distance_pos}
            my_note = {'YOU ARE THE HIDER':note_pos}
            my_buttons = {'READY':button1_pos, button2_text:button2_pos}

        # Process touch on screen
        for event in pygame.event.get():
            if(event.type is MOUSEBUTTONDOWN):
                pos = pygame.mouse.get_pos()
            elif(event.type is MOUSEBUTTONUP):
                pos = pygame.mouse.get_pos()
                x,y = pos

        # Update all the buttons and text on the screen
        screen.fill(BLACK)
        for my_text, text_pos in my_distance.items():
            update_text(my_font_distance,my_text,text_pos,WHITE)
        for my_text, text_pos in my_note.items():
            update_text(my_font_note,my_text,text_pos,RED)
        for my_text, text_pos in my_buttons.items():
            update_text(my_font_button,my_text,text_pos,WHITE)
        pygame.display.flip()

    GPIO.cleanup()
    pygame.quit()   
        
    ############################################################################
    # RPI Communication Cleanup
    ############################################################################
    my_control = -1
    file_write=ftp.file('/home/gnt4/FinalProject/h2s.txt', "w", -1)
    file_write.write(str(my_control)+','+str(my_control_time)+','+str(my_lat)+','+str(my_lon))
    file_write.flush()
    ftp.close()
    ssh.close()
  

seekerFinal.py


    # Grace Tan (gnt4) and Alan Hsiao (ah668)
    # ECE 5725 - Embedded OS - Final Project
    # Wednesday Night Lab
    # Displaying screen for seeker

    import os, sys, serial, time, math, random
    import RPi.GPIO as GPIO
    import pygame
    from pygame.locals import * # event MOUSE variables
    import pynmea2
    import paramiko
    import warnings
    from decimal import Decimal
    from cryptography.utils import DeprecatedIn25

    ############################################################################
    # RPi Communication
    ############################################################################
    warnings.simplefilter('ignore', DeprecatedIn25)
    ssh = paramiko.SSHClient()
    ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
    # Changed to generic information for security purposes
    ssh.connect('class_sever_ip', username='my_netid', password='my_password')
    ftp = ssh.open_sftp()

    # GPS Module
    ser = serial.Serial('/dev/ttyACM0', 9600, timeout=1)

    ############################################################################
    # Pygame 
    ############################################################################
    # Setup environment variables to display piTFT
    os.putenv('SDL_VIDEODRIVER', 'fbcon') # Display on piTFT
    os.putenv('SDL_FBDEV', '/dev/fb0') # Run on RPi and will display on piTFT
    os.putenv('SDL_MOUSEDRV', 'TSLIB') # Track mouse clicks on piTFT
    os.putenv('SDL_MOUSEDEV', '/dev/input/touchscreen')

    # Start pygame
    pygame.init()
    pygame.mouse.set_visible(False)


    # Variables for colors
    WHITE = 255, 255, 255
    BLACK = 0, 0, 0
    RED = 255, 0, 0
    GREEN = 0, 255, 0
    BLUE = 0, 0, 255

    # Set screen and coordinates
    size = width, height = 320, 240
    screen = pygame.display.set_mode(size)
    center = 160, 120

    # Fonts
    my_font_distance = pygame.font.Font(None, 70)
    my_font_button = pygame.font.Font(None, 20)
    my_font_note = pygame.font.Font(None,15)

    # Buttons
    button1_text = 'READY' # default is READY
    button2_text = 'QUIT'
    button3_text = ''
    button4_text = ''
    button1_pos = 290, 40
    button2_pos = 290, 225
    button3_pos = 290, 162
    button4_pos = 290, 102
    my_buttons = {button1_text:button1_pos, button2_text:button2_pos}

    # Notes/Messages
    note_text = 'YOU ARE THE SEEKER'
    note_pos = 160, 180
    my_note = {note_text:note_pos}

    # Distance
    distance_text = "-- m"
    distance_pos = center
    my_distance = {distance_text:distance_pos}

    # Function to display text 
    def update_text(font,text, pos, color):
        text_surface = font.render(text, True, color)
        rect = text_surface.get_rect(center = pos)
        screen.blit(text_surface,rect) 

    ############################################################################
    # GPIO 
    ############################################################################
    # Set for broadcom numbering
    GPIO.setmode(GPIO.BCM)

    # Setup piTFT buttons from top to bottom
    GPIO.setup(17, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(22, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(23, GPIO.IN, pull_up_down=GPIO.PUD_UP)
    GPIO.setup(27, GPIO.IN, pull_up_down=GPIO.PUD_UP)

    # Define events
    def GPIO_17_callback(channel):
        global my_control, other_control, my_control_time
        # If you press READY
        if my_control == 0:
            my_control = 1
            my_control_time = time.time() + 64 # 60 sec for actual game
        # If you press RESTART
        elif my_control == 6:
            my_control = 0
        # If you press PAUSE
        elif my_control == 4 and (other_control == 4 or other_control == 5):
            my_control = 5
        # If you press RESUME
        elif my_control == 5 and other_control >= 4:
            my_control = 4
            
    def GPIO_22_callback(channel):
        global has_powerup, powerup_time, my_control
        # If you press ZAP
        if has_powerup == 1:
            powerup_time = time.time() + 10
            my_control = 7
            has_powerup = 0

    def GPIO_23_callback(channel):
        global my_control, other_control, can_finish
        # If you press FINISH
        if my_control == 4 and other_control == 4 and can_finish:
            my_control = 6

    def GPIO_27_callback(channel):
        global quit_confirmed, quit_pressed_time
        # If you press QUIT
        quit_confirmed += 1 
        if quit_confirmed == 1:
            quit_pressed_time = time.time()
        
    # Add events for GPIO
    GPIO.add_event_detect(17, GPIO.FALLING, callback=GPIO_17_callback, bouncetime=300)
    GPIO.add_event_detect(22, GPIO.FALLING, callback=GPIO_22_callback, bouncetime=300)
    GPIO.add_event_detect(23, GPIO.FALLING, callback=GPIO_23_callback, bouncetime=300)
    GPIO.add_event_detect(27, GPIO.FALLING, callback=GPIO_27_callback, bouncetime=300)

    ############################################################################
    # Game
    ############################################################################
    # Game Control Variables
    # -1 -> not in game
    # 0  -> initialization
    # 1  -> pressed READY
    # 2  -> hider countdown
    # 3  -> hider finishes earlier - game starting countdown
    # 4  -> play game
    # 5  -> pressed PAUSE
    # 6  -> game over
    # 7  -> pressed ZAP
    my_control = 0 
    other_control = 0 

    # Other Game Variables
    my_control_time = 0     # Time to stop for countdowns
    other_control_time = 0  # Time to stop for countdowns
    powerup_time = 0        # Time when powerup finishes
    has_powerup = 0         # 1 when player has powerup and hasn't used it
    random_num = 1          # Random number rolled
    can_finish = 0          # 0 = does not show button, 1 = show button if less than 10 m
    quit_confirmed = 0      # 0 = not pressed, 1 = pressed QUIT, 2 = quit confirm
    quit_pressed_time = time.time()
    init_time = time.time()

    # GPS Variables
    my_lat = 0
    my_lon = 0
    other_lat = 0 
    other_lon = 0
    pi_distance = 0

    while quit_confirmed < 2:   

        # Write and read controls information to communicate to other RPi
        file_write=ftp.file('/home/gnt4/FinalProject/s2h.txt', "w", -1)
        file_write.write(str(my_control)+','+str(my_control_time)+','+str(my_lat)+','+str(my_lon))
        file_write.flush()
        file_read=ftp.file('/home/gnt4/FinalProject/h2s.txt', "r", -1)
        received_controls = file_read.read()
        if len(received_controls) > 0:
            parsed_receive_controls = received_controls.split(',')
            other_control = int(parsed_receive_controls[0])
            other_control_time = float(parsed_receive_controls[1])
            other_lat = float(parsed_receive_controls[2])
            other_lon = float(parsed_receive_controls[3])
        file_read.flush()

        # When QUIT is pressed, confirm quitting game with 5 second timeout
        if quit_confirmed == 1:
            button2_text = "CONFIRM QUIT?"
            button2_pos = 255,225
            if quit_pressed_time + 5 < time.time():
                quit_confirmed = 0
                button2_text = "QUIT"
                button2_pos = 290,225
            my_buttons = {button1_text:button1_pos, button2_text:button2_pos}
            
        # When game is being played and unpaused
        if (my_control == 4 or my_control == 7) and (other_control == 4 or other_control == 7):
            if time.time() > powerup_time:
                my_control = 4

            # Acquire GPS Data
            gps_line = ser.readline()
            if len(gps_line) == 0:
                print("Time out! exit.\n")
                sys.exit()
            # if gps_line.find('GGA') > 0:
            while gps_line.find('GGA') < 0:
                gps_line = ser.readline()
            gps_data = pynmea2.parse(gps_line)
            my_lat = float(gps_data.latitude)
            my_lon = float(gps_data.longitude)

            # Calculate GPS Distance with Haversine Formula
            lat1 = math.radians(my_lat) 
            lon1 = math.radians(my_lon) 
            lat2 = math.radians(other_lat) 
            lon2 = math.radians(other_lon) 
            dlon = lon2-lon1
            dlat = lat2-lat1
            earth_radius = 6373 # Average earth radius in km
            h1 = math.sin(dlat / 2)**2 + math.cos(lat1) * math.cos(lat2) * math.sin(dlon / 2)**2
            h2 = 2 * math.atan2(math.sqrt(h1), math.sqrt(1 - h1))
            pi_distance = round((earth_radius * h2 *1000),2)

            # When a random number is generated since there is currently no powerup
            if has_powerup == 0:
                random_num = random.randint(0,100)
            # If you get a powerup
            if random_num == 0:
                has_powerup = 1
                button4_text = 'ZAP'
            else:
                button4_text = ''

            # Print distance unless if there is no signal or other player has powerup
            if pi_distance > 10000:
                can_finish = 0
                my_distance = {'## m':distance_pos}
                button3_text = ''
            elif other_control == 7:
                my_distance = {'?? m':distance_pos}
                button3_text = ''
            else:
                my_distance = {str(pi_distance)+' m':distance_pos}
                if pi_distance < 10:
                    can_finish = 1
                    button3_text = 'FINISH'
                else:
                    can_finish = 0
                    button3_text = ''

            # Pause button and note
            if other_control == 4 and my_control != 7:
                button1_text = 'PAUSE'
            else:
                button1_text = ''
            my_note = {note_text:note_pos}
            my_buttons = {button1_text:button1_pos, button2_text:button2_pos, button3_text:button3_pos, button4_text: button4_pos}

        # If game is paused by both players
        elif my_control == 5 and other_control == 5:
            my_distance = {'PAUSED':distance_pos}
            my_note = {'HIDER ALSO PAUSED':note_pos}
            my_buttons = {'RESUME':button1_pos, button2_text:button2_pos}

        # If game is paused by you
        elif my_control == 5 and other_control == 4:
            my_distance = {'PAUSED':distance_pos}
            my_note = {'':note_pos}
            my_buttons = {'RESUME':button1_pos, button2_text:button2_pos}

        # If game is paused by other player
        elif my_control == 4 and other_control == 5:
            my_distance = {'PAUSED':distance_pos}
            my_note = {'WAITING FOR HIDER TO UNPAUSE...':note_pos}
            my_buttons = {'PAUSE':button1_pos, button2_text:button2_pos}

        # If game is finished and other player has restarted
        elif my_control == 6 and (other_control == 0 or other_control == 1):
            my_distance = {'GAME OVER':distance_pos}
            my_note = {'HIDER WANTS TO PLAY AGAIN!':note_pos}
            my_buttons = {'RESTART':button1_pos, button2_text:button2_pos}

        # If game is finished and other player has left game
        elif my_control == 6 and (other_control == -1):
            my_distance = {'GAME OVER':distance_pos}
            my_note = {'HIDER HAS LEFT GAME':note_pos}
            my_buttons = {'RESTART':button1_pos, button2_text:button2_pos}

        # If game is finished
        elif my_control == 6:
            my_distance = {'GAME OVER':distance_pos}
            my_note = {'THANKS FOR PLAYING!':note_pos}
            my_buttons = {'RESTART':button1_pos, button2_text:button2_pos}

        # If hider finishes hiding before countdown
        elif my_control == 2 and (other_control == 3 or other_control == 4):
            if other_control_time > time.time():
                my_distance = {str(int(other_control_time-time.time()))+' s':distance_pos}
                my_note = {'HIDER IS DONE! GAME IS STARTING':note_pos}
                my_buttons = {'':button1_pos, button2_text:button2_pos}
            else:
                my_control = 4

        # Hider Hiding
        elif my_control == 2 and other_control >= 2:
            if my_control_time > time.time():
                my_distance = {str(int(my_control_time-time.time()))+' s':distance_pos}
                my_note = {'HIDER IS HIDING':note_pos}
                my_buttons = {'':button1_pos, button2_text:button2_pos}
            else:
                my_control = 4

        # If both players have pressed START
        elif my_control == 1 and other_control >= 1:
            if my_control_time < other_control_time:
                my_control_time = other_control_time
            my_control = 2

        # If game has not started and only the other player has pressed READY
        elif my_control == 0 and other_control == 1:
            my_note = {'HIDER IS READY':note_pos}
            
        # If game has not started and only you have pressed READY
        elif my_control == 1 and other_control == 0:
            my_distance = {'-- m':distance_pos}
            my_note = {'HIDER IS NOT READY':note_pos}
            my_buttons = {'':button1_pos, button2_text:button2_pos}

        # If other player leaves game
        elif other_control == -1:
            my_control = 0
            my_distance = {'-- m':distance_pos}
            my_note = {'HIDER IS NOT IN GAME':note_pos}
            my_buttons = {'':button1_pos, button2_text:button2_pos}
        
        # If game has not begun
        else:
            my_distance = {'-- m':distance_pos}
            my_note = {'YOU ARE THE SEEKER':note_pos}
            my_buttons = {'READY':button1_pos, button2_text:button2_pos}

        # Process touch on screen
        for event in pygame.event.get():
            if(event.type is MOUSEBUTTONDOWN):
                pos = pygame.mouse.get_pos()
            elif(event.type is MOUSEBUTTONUP):
                pos = pygame.mouse.get_pos()

        # Update all the buttons and text on the screen
        screen.fill(BLACK)
        for my_text, text_pos in my_distance.items():
            update_text(my_font_distance,my_text,text_pos,WHITE)
        for my_text, text_pos in my_buttons.items():
            update_text(my_font_button,my_text,text_pos,WHITE)
        for my_text, text_pos in my_note.items():
            update_text(my_font_note,my_text,text_pos,RED)
        pygame.display.flip()    
        
    GPIO.cleanup()
    pygame.quit()

    ############################################################################
    # RPI Communication Cleanup
    ############################################################################
    my_control = -1
    file_write=ftp.file('/home/gnt4/FinalProject/s2h.txt', "w", -1)
    file_write.write(str(my_control)+','+str(my_control_time)+','+str(my_lat)+','+str(my_lon))
    file_write.flush()
    ftp.close()
    ssh.close()